﻿/*
 * Copyright (c) Martin Kinkelin
 *
 * See the "License.txt" file in the root directory for infos
 * about permitted and prohibited uses of this code.
 */

using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Diagnostics;

namespace Jarf
{
	/// <summary>
	/// Provides means to start/abort Rsync operations and encapsulates
	/// progress tracking and output/error handling.
	/// </summary>
	public partial class RsyncOperationControl : UserControl
	{
		/// <summary>
		/// Current child process.
		/// </summary>
		private RsyncProcess _process;



		/// <summary>
		/// Gets the task of the current operation.
		/// </summary>
		[Browsable(false)]
		public BackupTask Task
		{
			get
			{
				if (_process == null)
					throw new InvalidOperationException("No operation has been started yet.");

				return _process.Task;
			}
		}

		/// <summary>
		/// Gets a value indicating whether the current operation is a restore or backup operation.
		/// </summary>
		[Browsable(false)]
		public bool Restoring { get; private set; }

		/// <summary>
		/// Gets a value indicating whether an operation is currently running.
		/// </summary>
		[Browsable(false)]
		public bool IsRunning { get { return (_process != null && _process.IsRunning); } }



		/// <summary>
		/// Fired when the operation has been started.
		/// </summary>
		public event EventHandler Started;

		/// <summary>
		/// Fired when the operation has either been aborted or completed erroneously.
		/// </summary>
		public event EventHandler Aborted;

		/// <summary>
		/// Fired when the operation has completed successfully.
		/// </summary>
		public event EventHandler Completed;



		public RsyncOperationControl()
		{
			InitializeComponent();
		}

		~RsyncOperationControl()
		{
			if (_process != null)
				_process.Dispose();
		}


		/// <summary>
		/// Starts a backup or restore operation.
		/// </summary>
		/// <param name="task">Task to be backed up/restored.</param>
		/// <param name="restore">Indicates whether a restore or backup operation is requested.</param>
		/// <param name="simulateFirst">
		/// Indicates whether a simulation should take place before the real operation.
		/// After the simulation, the user will be prompted to confirm the pending changes.
		/// </param>
		public void Start(BackupTask task, bool restore, bool simulateFirst)
		{
			if (task == null)
				throw new ArgumentNullException("task");

			Restoring = restore;

			if (_process != null)
				_process.Dispose();

			try
			{
				Process process = (Restoring ? ProcessFactory.PrepareRestore(task, false) :
					ProcessFactory.PrepareBackup(task));

				if (simulateFirst)
				{
					// start a dry run
					process.StartInfo.Arguments = "-n " + process.StartInfo.Arguments;

					_process = new RsyncProcess(task, process);

					_process.Started += new EventHandler(SimulationProcess_Started);
					_process.Exited += new EventHandler(SimulationProcess_Exited);
				}
				else
				{
					_process = new RsyncProcess(task, process);

					_process.Started += new EventHandler(RealProcess_Started);
					_process.LineWritten += new EventHandler<DataReceivedEventArgs>(RealProcess_LineWritten);
					_process.Exited += new EventHandler(RealProcess_Exited);

					progressBar1.Maximum = 100;
				}

				_process.Start();

				OnStarted(EventArgs.Empty);
			}
			catch (Exception e)
			{
				MessageBox.Show(e.Message, (Restoring ? "Error while initializing restore" :
					"Error while initializing backup"), MessageBoxButtons.OK, MessageBoxIcon.Error);
			}
		}


		#region Process event handlers.

		/// <summary>
		/// Invoked when a simulation process (a dry run) has been started.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void SimulationProcess_Started(object sender, EventArgs e)
		{
			statusToolStripStatusLabel.Text = "Analyzing...";
			statusToolStripStatusLabel.Visible = true;

			displayOutputToolStripStatusLabel.Visible = false;

			abortToolStripDropDownButton.Visible = true;

			progressBar1.Style = ProgressBarStyle.Marquee;
			progressBar1.Value = 0;

			progressBar1.Left = 1 + statusToolStripStatusLabel.Width;
			progressBar1.Width = Width - (abortToolStripDropDownButton.Width + 16 + progressBar1.Left);

			progressBar1.Visible = true;

		}

		/// <summary>
		/// Invoked when a real (non-simulation) process has been started.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void RealProcess_Started(object sender, EventArgs e)
		{
			statusToolStripStatusLabel.Text = (Restoring ? "Restoring..." : "Backing up...");
			statusToolStripStatusLabel.Visible = true;

			displayOutputToolStripStatusLabel.Visible = false;

			abortToolStripDropDownButton.Visible = true;

			progressBar1.Value = 0;

			progressBar1.Left = 1 + statusToolStripStatusLabel.Width;
			progressBar1.Width = Width - (abortToolStripDropDownButton.Width + 16 + progressBar1.Left);

			progressBar1.Visible = true;
		}


		/// <summary>
		/// Invoked when a real (non-simulation) process has written a line to stdout or stderr.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void RealProcess_LineWritten(object sender, DataReceivedEventArgs e)
		{
			// increment the progress by one every time a line is written until
			// the maximum value is reached; then the value starts at 0 again
			if (progressBar1.Value == progressBar1.Maximum)
				progressBar1.Value = 0;
			else
				++progressBar1.Value;
		}


		/// <summary>
		/// Invoked when a simulation process (a dry run) has exited.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void SimulationProcess_Exited(object sender, EventArgs e)
		{
			statusToolStripStatusLabel.Visible = false;

			progressBar1.Visible = false;
			progressBar1.Style = ProgressBarStyle.Blocks;

			abortToolStripDropDownButton.Visible = false;

			CheckExitCode();

			if (_process.ExitCode != 0)
			{
				OnAborted(EventArgs.Empty);
				return;
			}

			using (SimulationResultDialog dialog = new SimulationResultDialog(_process, Restoring))
			{
				if (dialog.ShowDialog(this) != DialogResult.OK)
				{
					OnAborted(EventArgs.Empty);
					return;
				}
			}

			// set the progress range to the number of relevant lines before
			// the stats block
			progressBar1.Maximum = Math.Max(_process.Output.Count - 16, 0);

			// start the real process
			ProcessStartInfo startInfo = _process.Process.StartInfo;
			startInfo.Arguments = startInfo.Arguments.Substring(3);

			try
			{
				Process process = new Process();
				process.StartInfo = startInfo;

				_process.Dispose();

				_process = new RsyncProcess(_process.Task, process);

				_process.Started += new EventHandler(RealProcess_Started);
				_process.LineWritten += new EventHandler<DataReceivedEventArgs>(RealProcess_LineWritten);
				_process.Exited += new EventHandler(RealProcess_Exited);

				_process.Start();
			}
			catch (Exception exception)
			{
				MessageBox.Show(exception.Message, (Restoring ? "Error while initializing restore" :
					"Error while initializing backup"), MessageBoxButtons.OK, MessageBoxIcon.Error);

				OnAborted(EventArgs.Empty);
			}
		}

		/// <summary>
		/// Invoked when a real (non-simulation) process has exited.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void RealProcess_Exited(object sender, EventArgs e)
		{
			statusToolStripStatusLabel.Visible = false;
			progressBar1.Visible = false;
			abortToolStripDropDownButton.Visible = false;

			displayOutputToolStripStatusLabel.Visible = true;

			CheckExitCode();

			if (_process.ExitCode == 0)
				OnCompleted(EventArgs.Empty);
			else
			{
				OnAborted(EventArgs.Empty);

				string formatString = (!Restoring ?
					"Error while trying to back up \"{0}\" to \"{1}\":\n\n{2}" :
					"Error while trying to restore \"{0}\" from \"{1}\":\n\n{2}");

				Log.Instance.Write(EventLogEntryType.Error,
					string.Format(formatString, Task.Source, Task.Destination, _process.FullOutput));
			}
		}

		#endregion

		private void displayOutputToolStripStatusLabel_Click(object sender, EventArgs e)
		{
			DisplayOutput();
		}

		private void abortToolStripDropDownButton_Click(object sender, EventArgs e)
		{
			Abort();
		}


		private void OnStarted(EventArgs e)
		{
			if (Started != null)
				Started(this, e);
		}

		private void OnAborted(EventArgs e)
		{
			if (Aborted != null)
				Aborted(this, e);
		}

		private void OnCompleted(EventArgs e)
		{
			if (!Restoring)
				Task.LastBackup = DateTime.Now;

			if (Completed != null)
				Completed(this, e);

			string formatString = (!Restoring ?
				"\"{0}\" successfully backed up to \"{1}\":\n\n{2}" :
				"\"{0}\" successfully restored from \"{1}\":\n\n{2}");

			Log.Instance.Write(EventLogEntryType.Information,
				string.Format(formatString, Task.Source, Task.Destination, _process.FullOutput));
		}


		/// <summary>
		/// Prompts the user to abort the current operation.
		/// Returns true if the user chose to abort.
		/// </summary>
		/// <returns></returns>
		public bool Abort()
		{
			if (MessageBox.Show("Are you sure you want to abort the current operation?", "Confirmation",
				MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
			{
				return false;
			}

			// check if the process is still running
			if (!_process.IsRunning)
				return true;

			_process.Kill();

			bool simulating = (progressBar1.Style == ProgressBarStyle.Marquee);

			statusToolStripStatusLabel.Visible = false;

			progressBar1.Visible = false;
			progressBar1.Style = ProgressBarStyle.Blocks;

			abortToolStripDropDownButton.Visible = false;

			displayOutputToolStripStatusLabel.Visible = (_process.FullOutput.Length > 0);

			OnAborted(EventArgs.Empty);

			if (!simulating)
			{
				string formatString = (!Restoring ?
					"Backup of \"{0}\" to \"{1}\" aborted:\n\n{2}" :
					"Restoring \"{0}\" from \"{1}\" aborted:\n\n{2}");

				Log.Instance.Write(EventLogEntryType.Warning,
					string.Format(formatString, Task.Source, Task.Destination, _process.FullOutput));
			}

			return true;
		}

		/// <summary>
		/// Displays the output of the current process in a dialog.
		/// </summary>
		private void DisplayOutput()
		{
			using (OutputForm form = new OutputForm("Rsync output", _process.FullOutput))
			{
				form.ShowDialog(this);
			}
		}

		/// <summary>
		/// Checks the exit code of a Rsync child process and displays
		/// a message box if an error occurred.
		/// </summary>
		private void CheckExitCode()
		{
			if (_process.ExitCode == 0)
				return;

			string message = null;

			switch (_process.ExitCode)
			{
				case 1:
					message = "Syntax or usage error";
					break;
				case 2:
					message = "Protocol incompatibility";
					break;
				case 3:
					message = "Errors selecting input/output files, dirs";
					break;
				case 4:
					message = "Requested action not supported: an attempt was made to manipulate 64-bit files\n" +
										"on a platform that cannot support them; or an option was specified that is\n" +
										"supported by the client and not by the server.";
					break;
				case 5:
					message = "Error starting client-server protocol";
					break;
				case 6:
					message = "Daemon unable to append to log-file";
					break;
				case 10:
					message = "Error in socket I/O";
					break;
				case 11:
					message = "Error in file I/O";
					break;
				case 12:
					message = "Error in rsync protocol data stream";
					break;
				case 13:
					message = "Errors with program diagnostics";
					break;
				case 14:
					message = "Error in IPC code";
					break;
				case 20:
					message = "Received SIGUSR1 or SIGINT";
					break;
				case 21:
					message = "Some error returned by waitpid()";
					break;
				case 22:
					message = "Error allocating core memory buffers";
					break;
				case 23:
					message = "Partial transfer due to error";
					break;
				case 24:
					message = "Partial transfer due to vanished source files";
					break;
				case 25:
					message = "The --max-delete limit stopped deletions";
					break;
				case 30:
					message = "Timeout in data send/receive";
					break;
				case 35:
					message = "Timeout waiting for daemon connection";
					break;
				default:
					message = "Unknown error";
					break;
			}

			if (MessageBox.Show(string.Format("An error has occurred: {0}\n\"{1}\"\n\nWould you like to display the output?",
				_process.ExitCode, message), (Restoring ? "Error during restore" : "Error during backup"),
				MessageBoxButtons.YesNo, MessageBoxIcon.Error) == DialogResult.Yes)
			{
				DisplayOutput();
			}
		}
	}
}
